Skip to content

ci: add make ci-remote and make the repo's hooks fail closed - #798

Merged
EricAndrechek merged 14 commits into
mainfrom
ci/remote-make-ci-v2
Oct 9, 2026
Merged

EricAndrechek merged 14 commits into
mainfrom
ci/remote-make-ci-v2

Conversation

@EricAndrechek

Copy link
Copy Markdown
Member

This PR has two parts:

  1. make ci-remote runs the pre-push gate's make ci on another machine.
  2. The repo's hooks now fail closed. These are git's .githooks/ and the Claude Code hooks in .claude/. Each one failed the wrong way when a file it runs was missing, or when an action came in a form the hook didn't recognise (fix(hooks): pre-commit fails in any worktree without a Makefile, so a non-code worktree cannot commit #678, fix(agents): review-gate hooks fail open when their scripts are missing #787, fix(agents): the push gate misses gh pr new and the REST/GraphQL forms of PR operations #790).

They share the pre-push hook, the verify leaves and the hook docs, so they ship together and their overlaps are resolved once.

Part 1: make ci-remote

A code push needs tmp/ci-passed-tree-<tree>, and only a make ci in the same checkout wrote it. So every push paid for a full local run (race detector, coverage, a Docker daemon carrying every suite's containers), and the gate only ever exercised the contributor's own OS and architecture.

make ci-remote HOST=<host> runs that make ci on another machine and keeps the gate's meaning.

  • Transports. HOST is any ssh destination (a ~/.ssh/config alias works), or docker:<container> for a local container or devcontainer. Over ssh the commands run in a login shell; in a container, in its own environment.
  • What travels. The worktree must be clean. HEAD is streamed as a git bundle on the transport's stdin, right after the host-side script, so nothing is pushed. The host checks out that commit in a fresh directory under REMOTE_DIR, runs make tools && make ci, streams the output back with its uname -sm first, and removes the directory unless KEEP=1. Stopping the command, by Ctrl-C or a signal to the script alone, stops the run on the host: the host's stdin stays open only while the script runs, and a watcher stops the run's process group when it closes.
  • When the marker is written. Only if all of these hold: the host exits 0; the tree it reports having checked out is HEAD^{tree}; make ci left its checkout clean; and the local worktree is still clean and on that tree at the end. The host's report lines carry a per-run nonce, so make ci's own output can't forge them. The verify marker is written too, as make ci does.
  • Provenance. tmp/ci-passed-tree-<tree>.provenance records the host, OS/arch, start and end times, and the run's own local copy of the log (tmp/ci-remote-<tree>-<run>.log, so concurrent runs of one tree don't overwrite each other). The pre-push hook prints make ci passed on <host> (<os>/<arch>) from it. Marker semantics and local make ci are unchanged.
  • Docs. A make ci-remote section in the development guide (usage, host requirements, how the gate treats it), the AGENTS.md pre-push rules, the hook table, and CONTRIBUTING.md's pre-push sentence.

Part 2: hooks that fail closed

Each now fails closed and says why.

#678: git hooks in a worktree with no Makefile

With an absolute core.hooksPath, git runs one checkout's .githooks/ in every worktree, including one on an orphan branch that keeps a tool's state and no code. There, pre-commit couldn't find scripts/ci-marker.sh, hid that behind 2>/dev/null, and ran make verify with no Makefile (No rule to make target 'verify').

  • pre-commit: no Makefile in the worktree means nothing to verify, so it exits 0 with a note. A missing scripts/ci-marker.sh, or one that fails with anything but "no marker" (exit 1), is now reported, and then make verify runs, which is the fail-closed path.
  • pre-push had the same failure, reported as a misleading "can't resolve tree". It now decides per pushed commit, not per worktree. A pushed commit with no Makefile holds no code to validate, so it goes through. One with a Makefile, pushed from a worktree that lacks scripts/ci-marker.sh, is blocked with a message naming the script. Deciding by worktree would have failed open: git push origin <code-branch> from a worktree with no code would have skipped the check.
  • The make ci-remote test needed one fix for this. Its scratch repository had no Makefile. So once the pre-push hook passed a commit without one, the test's two pre-push cases ("blocks a code push with no marker" and "accepts the marker and names the host") stopped exercising the marker check and failed. A code repository has a Makefile, so the scratch one now does too, in its own commit.

On core.hooksPath. This PR doesn't change it. make tools sets the relative .githooks, and git resolves a relative core.hooksPath in each worktree (checked with git 2.56). So each worktree runs its own branch's hooks, and a worktree whose branch has no .githooks/ runs no hooks at all. That fails open, the class of bug #787 is about. #678 was found with an absolute path, which runs one checkout's hooks everywhere. Switching to a relative path would have "fixed" #678 by turning a loud failure into a silent one. So the fix is in the hooks, which now behave correctly under either setting.

#787: Claude Code hooks whose scripts are missing

Each hook in .claude/settings.json was a bare $CLAUDE_PROJECT_DIR/.claude/hooks/<script>.sh. CLAUDE_PROJECT_DIR is fixed when the session starts. Once that checkout is removed, or a script loses its executable bit, the shell exits 127 or 126. Claude Code treats that as a non-blocking error: "When the script path doesn't exist or isn't executable, the shell exits with a code like 127 … For most hook events, the action proceeds. … a mistyped path in settings.json leaves the gate silently disabled."

The scripts may be the thing that's missing, so the fix has to live in the commands. Each is now a one-line POSIX sh guard; sh -c is how Claude Code runs a shell-form hook on macOS and Linux.

When the script is there and executable, the guard execs it, with the same process, stdin, argv and environment as before. The only visible change is that the path is now quoted, so a project directory with a space in it works.

When the script isn't there, the guard acts by event:

Event Script missing or not executable → Why this code
PreToolUse (Bash) exit 2 for any command containing the words git and push; gh and pr with one of create, new, edit, ready, review or merge; or gh and api with pulls or graphql. That last rule blocks every call to the pull-request or GraphQL API, reads included. Also exit 2 for every command when jq is missing or the payload can't be parsed. Every other command: exit 0. "Exit 2 means a blocking error … PreToolUse blocks the tool call", and exit 2 is "the only exit code that blocks through the code alone" (Exit code 2).
SubagentStop exit 1, with a one-line stderr message For SubagentStop, exit 2 "Prevents the subagent from stopping" (per-event table), which risks a loop. A non-blocking error shows "a <hook name> hook error notice followed by the first line of stderr" (Other exit codes), so the message is one line. No marker gets written, so a push is still blocked, by the push gate or by the PreToolUse fallback above.
PostToolUse (both formatters) exit 2, with a stderr message For PostToolUse, exit 2 "Shows stderr to Claude; the tool already ran" (per-event table). The docs recommend exit 2 for exactly this case: "To surface a warning to Claude from a PostToolUse … hook, exit 2 instead" (Exit code 0). Stderr from exit 0 "goes to the debug log only … Claude never sees it", and exit 1 only reaches the transcript notice.

Each message names the missing script and says how to recover: restart Claude Code from a live checkout.

Design notes:

  • Why not block every Bash call? A session whose launch checkout has gone can still be working in other worktrees, and it needs Bash to notice the problem and recover. Blocking only what the gate guards keeps the gate's guarantee. The exception is a missing jq: then the command can't be read, so every command is blocked, which is what agent-bash-gate.sh already does without jq.
  • The fallback's detection is crude on purpose. It matches whole words anywhere in the command text, ignoring quoting, so it over-blocks: git stash push, git push mentioned in a commit message, and a read-only gh api …/pulls/N/comments are all blocked. It never runs while the real gate can run, and like the gate it guards against accidents, not deliberate workarounds. Only .tool_input.command decides; the Bash tool's description field is ignored.
  • jq isn't guaranteed. It's a documented prerequisite (make verify and the hooks need it), not a given, so its absence fails closed.
  • Every fallback reads its payload to the end before exiting, so Claude Code isn't left half-way through writing a large Write payload to a hook that never reads it.
  • A script that exists but can't start (a broken shebang, say) still fails open. The guard checks that the script exists and is executable; catching a failed exec would mean giving up the exec.

#790: the agent gate missed the alias and API forms of what it guards

agent-bash-gate.sh matched only the gh pr spellings, so each guarded action went through in another form:

  • gh pr new, an alias of gh pr create, skipped the --draft check and the title lint.
  • gh api …/pulls/N/reviews -f event=APPROVE approved a PR.
  • gh api …/pulls -f head=… -f title=… opened a non-draft PR.
  • gh api …/requested_reviewers -f 'reviewers[]=…' requested a reviewer. The check wanted -X POST or a literal reviewers=, though a field already makes gh api POST.
  • gh api -X PUT …/pulls/N/merge merged.
  • GraphQL markPullRequestReadyForReview marked a draft ready.

Each of these now gets the same answer as its gh pr form:

  • gh pr new is checked like gh pr create.
  • REST. A gh api call counts as a write when it names a method other than GET or, naming none, sends fields.
    • A write to …/requested_reviewers (DELETE included) is blocked.
    • A write to …/pulls/N/merge is blocked.
    • A write that opens a PR (…/pulls) needs draft=true. Its title= goes through the PR-title lint, as does a retitle (PATCH …/pulls/N).
    • A review with event=APPROVE or event=REQUEST_CHANGES is blocked.
  • GraphQL. These are blocked:
    • markPullRequestReadyForReview;
    • mergePullRequest and enablePullRequestAutoMerge;
    • requestReviews;
    • an addPullRequestReview or submitPullRequestReview that approves or requests changes;
    • a createPullRequest without draft: true.
  • gh pr merge stays with the deny rule in .claude/settings.json and out of the gate. A person merges with it from Claude Code's shell mode (! gh pr merge …), and the docs don't say whether PreToolUse hooks see shell-mode commands. The shell-mode section says only that they run "without going through Claude" and don't "require Claude to interpret or approve the command"; the hooks reference doesn't mention them. Blocking it in the gate could break that workflow. The API and GraphQL merge forms don't overlap with it, so the gate blocks them.

The checks now read what the line runs, not what it mentions. The gh checks used to read the line with quoted strings removed and everything else kept. That made them wrong both ways:

  • They blocked a command that only mentioned a guarded action in a heredoc body or a comment, such as a python3 - <<'EOF' script with # gh pr ready in it.
  • They missed a guarded action inside a $(…) or handed to bash -c.

They now read the commands the line runs, through the gate's own tokenizer, the same view the push check already uses:

  • A mention in a quoted string, a heredoc body or a comment doesn't count.
  • Code a shell runs is queued, tokenized and judged the same way. That covers a $(…), eval's words, a heredoc a shell reads directly or through a pipe, and the code a shell is handed with -c. Options before -c are stepped over, including the value of -o/+o: bash -euo pipefail -c '…' and bash --noprofile --norc -c '…' both count. A short-option cluster containing c counts as -c; a long option such as --norc doesn't.
  • The code inside a $(…) is read as written, quotes and heredoc bodies included. So url=$(gh pr create --draft --title "feat(x): …") is judged on its real title, and a heredoc a shell reads inside a $(…) counts too. (The push check still doesn't follow that last form.)
  • A GraphQL query= computed by a command (-f query="$(cat <<'EOF' … EOF)") has its guarded mutation names matched against the code that computes it, heredoc bodies included.
  • The gate follows 64 pieces of nested code. Past that, a piece left that mentions a gh pr or gh api command is refused, unjudged.
  • A line the tokenizer can't parse is refused when it mentions a gh pr or gh api command, because what it runs can't be told.

Each gh command is judged on its own words.

  • A gh api call is judged by its endpoint (its first argument that isn't a flag) and by its fields. draft=, title= and event= are read only from -f, -F, --field or --raw-field values, and GraphQL mutation names only from the query= field. So a review-thread reply or comment whose body names an endpoint, a field or a mutation doesn't count, and neither does another call on the same line.
  • gh's own -R/--repo before the command is stepped over (gh -R o/r pr ready 12, --repo=o/r, -Ro/r), for pr and api both.
  • A boolean flag set with = is read as gh reads it: --draft=true counts as --draft, and --approve=false doesn't approve.
  • Titles are read from the tokens, so an unquoted --title is linted too.

The gate still reads the command line only, so fields sent through --input or -F field=@file go unseen.

It runs under macOS's bash 3.2. On a Mac with no newer bash on PATH, #!/usr/bin/env bash finds /bin/bash 3.2. An earlier revision of this branch stopped there with "unbound variable" whenever a gh line had no substitution, because it expanded an empty array under set -u. The gate then exited 1, which Claude Code doesn't treat as a block. The scan now walks its arrays by index. The three hook tests run a second time with /bin/bash first on PATH when it is a different bash, so a construct 3.2 rejects fails a test.

Timing. The tokenizer used to step through a double-quoted string one character at a time; it now takes runs of plain text in one step. Measured on an earlier revision of this change (a 100 KB inline --body "…": 11 s with the old character loop, under 1 s after); not re-measured after the per-call rewrite.

Test plan

On this branch at its head, 9b8c30c (measured):

  • make verify passes with all 23 leaves.
  • make build-docs passes.
  • shellcheck v0.11.0 is clean on all 48 tracked *.sh and .githooks/* files.
  • The pre-commit make verify passed on each commit that went through it.
  • make ci: not run.

Part 1, make ci-remote. Carried over from the stacked version of this PR (#766) unless marked as re-run here.

  • scripts/ci/remote-ci.test.sh, a new make verify leaf (test-remote-ci): 31/31 on this branch (re-run here). It runs in a scratch repository, which now has a Makefile, with stand-in ssh/docker commands that run the host side for real, in its own process group as sshd would, against a stub make, uname and git. It covers:
    • a passing run: markers, provenance fields, the log, host cleanup;
    • KEEP and REMOTE_DIR;
    • a failing make ci;
    • two tree mismatches: HEAD moved during the run, and the host checked out another commit;
    • a worktree dirtied during the run, and make ci dirtying the host's checkout;
    • stopping the local process group, or only the script, mid-run; either way the host's make ci stops and its directory goes;
    • a report line forged in make ci's output;
    • dirty-worktree and bad-HOST refusals with status.showUntrackedFiles=no set;
    • the docker exec command line;
    • two runs of one tree keeping separate logs;
    • the pre-push hook with and without provenance.
  • Each check in the script and the hook was deleted in turn, and every deletion fails at least one case. The exception was the empty-report check, which was then folded into the tree comparison. (Carried over.)
  • Against a real Linux x86_64 host over ssh (carried over):
    • What worked: the bundle checked out at the right tree, the report lines came first, and phase 1 of make ci passed there, remote-ci test included.
    • What didn't: phase 2 needed host-side Docker networking setup. make ci ran inside a container on the host's network, so testcontainers dialed the bridge gateway and timed out; TESTCONTAINERS_HOST_OVERRIDE=localhost is the documented fix.
    • No marker was written for that run, and none when the local side was killed.
    • A full green make ci on a remote host has not been run yet.
  • ssh -T and docker exec -i both return within a second of the remote command exiting while local stdin is still open, which the interrupt watcher relies on. (Carried over.)

Part 2, hooks that fail closed (measured):

  • make test-githooks (scripts/githooks.test.sh, new verify leaf): 11/11 pass, under bash 5.3 and again with /bin/bash 3.2 first on PATH.
    • It builds a scratch repository whose core.hooksPath names this checkout's .githooks/ by absolute path, then commits and pushes from a code worktree and from an orphan worktree with no Makefile.
    • The test clears git's repository variables, uses git -C throughout, and refuses to write unless each scratch repository and worktree is where it asked.
    • Against the old hooks, 6 of 11 fail, including make: *** No rule to make target 'verify' for the commit with no Makefile.
  • make test-hook-commands (.claude/hooks/hook-commands.test.sh, new verify leaf): 171/171 pass under sh, dash and bash, and 171/171 again with /bin/bash 3.2 first on PATH.
    • It reads every hook command from the real settings.json and runs it under each shell.
    • It covers an empty CLAUDE_PROJECT_DIR, a non-executable copy of every script, the variable unset, and no jq.
    • It runs each command against stub scripts that record their stdin byte for byte, their argv and their exit code, including with a path containing a space.
    • It also runs this checkout's real scripts.
    • A catch-all case fails any hook command, present or future, that exits 0, 126 or 127 when its script is missing or not executable. Dropping [ -x "$h" ] && from the SubagentStop guard fails 6 cases.
    • Against the old settings.json, 141 of 171 fail. The 30 that pass are exactly the stub-script and real-checkout cases, which shows the commands behave as before when the scripts exist.
  • make test-review-gate: 230/230 pass under bash 5.3, and 230/230 with /bin/bash 3.2.57 first on PATH.
    • The 93 new cases cover every alias, REST and GraphQL form above.
    • Code inside $(…), read as written:
      • Blocked: a GraphQL ready mutation captured by $(…); a bad title is still linted there; a gh pr ready in a heredoc read by bash inside $(…).
      • Allowed: a draft PR's URL captured with a two-word title and with a scoped title; a comment body with an apostrophe; a grep -c 'gh pr ready'.
    • Look-alikes that must go through: GET on a PR, its reviews, its requested reviewers and its merge status; -X GET with fields; a COMMENT review; a GraphQL query; a GraphQL draft create; an issue comment that mentions approving and merging.
    • Mentions, allowed: a Python heredoc's comment, a cat heredoc, a shell comment, a quoted string, a PR-body heredoc.
    • A guarded command a shell runs, blocked: in a $(…), bash -c, bash -lc, sh -c or eval, in a heredoc read by bash or piped into sh, and behind timeout.
    • =-flags in both directions: --draft=true allowed, --draft=false blocked, --approve=true and --request-changes=true blocked, --approve=false --comment allowed.
    • Per-call judgment: a review-thread reply quoting a merge endpoint, a comment naming requested_reviewers or event=APPROVE, and a GraphQL comment naming a mutation are allowed. A reviewer read followed by a comment is allowed; a comment followed by a reviewer write is blocked. A non-draft create whose body says draft=true is blocked.
    • -R/--repo forms: blocked for ready, review --approve, create without --draft, edit --add-reviewer and an API merge. gh -R o/r pr view is allowed.
    • Nesting cap: a gh pr ready in the 63rd $(…) is judged and blocked, one past the 64 pieces the gate follows is refused, and a gh read beside 70 substitutions with no gh in them is allowed.
    • Heredoc GraphQL query: ready-for-review is blocked; a read is allowed.
    • Shell options before -c: -e -c, -euo pipefail -c, -o pipefail -c, +o posix -c and --noprofile --norc -c are all followed, by the gh check and the push check alike. bash --norc running a script and a read under bash -o pipefail -c are allowed.
    • Against main's gate: 49 of the 93 fail, under both bashes.
    • Against earlier revisions of this branch:
      • The revision before the last round failed 6 of the 7 newest cases, the $(…) ones above, under both bashes. Only "a bad title is still linted" passed there.
      • The round before that failed 12 of its 21 newest cases (cap, -R, heredoc query, shell options), under both bashes.
      • The one before that, under /bin/bash 3.2, gave 135 ok / 67 FAIL, every failure "TK_SUBS[@]: unbound variable".
  • make test-hook-commands also caught the bash 3.2 bug: "the real gate blocks gh pr ready" exited 1 instead of 2, in all three shells.
  • The inline guards pass shellcheck as sh and dash. make lint-sh now also checks the extensionless .githooks/ files.
  • Timing: measured on an earlier revision of this change (a 100 KB double-quoted --body: 11 s with the old character loop, under 1 s after); not re-measured after the per-call rewrite.

Related Issues

Closes #764
Closes #678
Closes #787
Closes #790
Supersedes #766 (rebuilt on main after #785).
Part of #740

Based on main. The two PRs it was stacked on, #793 (-trimpath) and #794 (the per-PR build cache), have merged. Gate forms review found but left out of this PR, as less common than the ones it covers: #797.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH

EricAndrechek and others added 11 commits October 9, 2026 12:15
A code push needs tmp/ci-passed-tree-<tree>, and only a make ci in the
same checkout wrote it, so every push paid for a full local run and the
gate only ever exercised the contributor's own OS and architecture.

make ci-remote HOST=<ssh destination>|docker:<container> streams HEAD to
the host as a git bundle on the transport's stdin, checks it out in a
fresh directory, runs make tools && make ci there and streams the output
back. The same markers are written locally only if the host exits 0, the
tree it reports having checked out is HEAD^{tree}, make ci left that
checkout clean, and the local worktree is clean before and after the run
and still on that tree. Report lines carry a per-run nonce so make's own
output can't forge them. A provenance file beside the marker records
where the run happened, and the pre-push hook prints it.

scripts/ci/remote-ci.test.sh (a make verify leaf) runs the script through
stand-in ssh and docker commands in a scratch repository.

The host run stops whenever the script exits, however it ends: the host
keeps its stdin open and a watcher stops the run's process group when it
closes. Each run writes its own log, named with its nonce. The docs
cover interrupts, host-network containers and the host's requirements.

Closes #764. Part of #740.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
The pre-push hook is about to pass a pushed commit that has no
Makefile, since it holds no code to validate (#678). This test's
scratch repository had none, so its pre-push cases would stop testing
the marker check: the push would pass as code-less, and "pre-push
blocks a code push with no marker" and "pre-push accepts the marker
and names the host" would fail. A code repository has a Makefile, so
the scratch one now does too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
With an absolute core.hooksPath, git runs one checkout's .githooks/ in
every worktree, including one on an orphan branch that keeps a tool's
state and no code. There pre-commit could not find scripts/ci-marker.sh,
hid the error behind 2>/dev/null, and ran `make verify` with no
Makefile ("No rule to make target 'verify'"), so nothing could be
committed (#678).

- pre-commit: a worktree with no Makefile has nothing to verify and
  exits 0. A missing scripts/ci-marker.sh, or one that fails other than
  "no marker" (exit 1), is reported and `make verify` runs.
- pre-push: a pushed commit with no Makefile holds no code to validate
  and goes through. One with a Makefile, pushed from a worktree without
  scripts/ci-marker.sh, is blocked with a message that says so, instead
  of "can't resolve tree". The decision follows what is pushed, not the
  worktree it is pushed from, so a code branch can't slip through a
  code-less worktree.
- scripts/githooks.test.sh (make test-githooks, a verify leaf) commits
  and pushes through the real hooks from scratch worktrees, with
  core.hooksPath pointing at them by absolute path.
- make lint-sh now shellchecks the extensionless .githooks/ files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
Each hook in .claude/settings.json ran
$CLAUDE_PROJECT_DIR/.claude/hooks/<script>.sh, and CLAUDE_PROJECT_DIR
is fixed when the session starts. Once that checkout was removed, or a
script went missing or lost its executable bit, the shell exited 127 or
126. Claude Code treats any exit code but 2 as a non-blocking error, so
the push gate stopped gating and the marker hook stopped writing
markers, silently (#787).

Each command is now a short POSIX sh guard. When the script is there
and executable it is exec'd with the same stdin, argv and environment as
before. Otherwise, by event:

- PreToolUse (Bash): exit 2 for any command containing the words `git`
  and `push`, or `gh` with `pr` or `api` and one of create, edit, ready,
  review, merge or requested_reviewers; and for every command when jq
  is missing or the payload is unreadable, as the gate itself does.
  Other commands go through, so the session can still work.
- SubagentStop: exit 1 with a one-line stderr message. Exit 2 would
  keep the subagent running; the push gate then finds no marker.
- PostToolUse: exit 2, which shows Claude the message after an edit
  that already happened.

Every fallback reads its payload to the end and names the missing
script and the fix: restart Claude Code from a live checkout. The
quoted path also fixes a project directory with a space in it.

.claude/hooks/hook-commands.test.sh (make test-hook-commands, a verify
leaf) reads each command from the real settings.json and runs it under
sh -c (and dash and bash when present) with an empty project directory,
CLAUDE_PROJECT_DIR unset, no jq, a non-executable script, stub scripts
that record their stdin and argv, and this checkout's real scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
Review follow-up to the fail-closed hook guards (#787).

- hook-commands.test.sh tested a script that is there but not
  executable only for PreToolUse, though the docs say every command is
  tested that way. It now runs every command in settings.json against a
  non-executable copy of each real script as well as against an empty
  project directory, with the same assertions (not exit 0, 126 or 127,
  and a message naming the script), plus the exact SubagentStop and
  PostToolUse codes. Dropping `[ -x "$h" ] &&` from the SubagentStop
  guard now fails 6 cases (exit 126); before, all passed.
- The PreToolUse fallback's word list let through the API and alias
  forms of what the gate guards: `gh pr new`, `gh api .../pulls/N/reviews
  -f event=APPROVE`, `gh api .../pulls -f head=...` (a non-draft PR) and
  a GraphQL markPullRequestReadyForReview. It now blocks `gh` and `pr`
  with create, new, edit, ready, review or merge, and `gh` and `api`
  with pulls or graphql: every pull-request or GraphQL API call, reads
  included. Still crude, still erring toward blocking, and only in play
  while the real gate can't run. The test, AGENTS.md, the Claude Code
  docs and the changelog entry list the new words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
agent-bash-gate.sh matched only the `gh pr` spellings, so each action
it guards went through in another form: `gh pr new` skipped the draft
check and the title lint; `gh api` approved a PR, opened a non-draft
one, requested a reviewer (`reviewers[]=` with no -X) and merged; and
GraphQL marked a draft ready.

- `gh pr new` is checked like `gh pr create`.
- A `gh api` call writes when it names a method other than GET or,
  naming none, sends fields. A write to requested_reviewers (DELETE
  included) is blocked; one to .../pulls/N/merge is blocked as a merge;
  one that opens a PR (.../pulls) needs draft=true, and its title= goes
  through the PR-title lint, as does a retitle (PATCH .../pulls/N).
- A review with event=APPROVE or REQUEST_CHANGES is blocked.
- GraphQL: markPullRequestReadyForReview, mergePullRequest,
  enablePullRequestAutoMerge, requestReviews, an approving or
  changes-requesting add/submitPullRequestReview, and a
  createPullRequest without draft: true are blocked.
- `gh pr merge` itself stays with the settings.json deny rule and out
  of the gate: a person merges with it from shell mode (`!`), and the
  Claude Code docs don't say whether PreToolUse hooks see those.

The gh checks read the line with quoted strings removed and the rest
kept, so a guarded command only mentioned in a heredoc body or a
comment was blocked, and one in a $(...) or `bash -c` was missed. They
now read the commands the line runs through the gate's own tokenizer,
like the push check; code a shell runs (a $(...), `bash -c`, a heredoc
a shell reads directly or through a pipe) still counts. Titles come
from the tokens, so an unquoted --title is linted too.

The tokenizer takes plain text inside double quotes in one step, so a
100 KB inline --body no longer costs about 11 s per command.

review-gate.test.sh: 44 new cases, the blocked forms, look-alikes that
read (GET pulls, reviews, requested_reviewers, merge status; a COMMENT
review; a GraphQL query) and mentions in heredocs and comments; 24 of
them fail against the previous gate.

Closes #790

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
Review follow-up to the agent gate's gh checks (#790).

- bash 3.2: `for s in "${TK_SUBS[@]}"` stopped the gate with "unbound
  variable" under `set -u` when a line had no substitution, on macOS's
  /bin/bash 3.2.57. The gate then exited 1, which Claude Code doesn't
  treat as a block, so every line mentioning gh went through, a push on
  the same line included. With the hooks under /bin/bash the review-gate
  suite gave 135 ok and 46 FAIL. The scan now walks its arrays by index
  and never expands one that may be empty.
- Code a shell runs is judged like the line itself: the word after
  `bash -c` (or -lc and the like), eval's words, each $(...) and
  backtick body, and a heredoc a shell reads are queued, tokenized and
  scanned in turn (at most 64). `bash -c 'gh api -X PUT .../merge'` and
  the like were allowed.
- Each gh command is judged on its own words. A gh api call is judged by
  its endpoint, its first argument that isn't a flag, and by its fields:
  draft=, title= and event= only from -f/-F/--field/--raw-field values,
  GraphQL mutation names only from query=. A review-thread reply whose
  body quoted a merge endpoint, an issue comment naming
  requested_reviewers, and a reviewer read followed by a comment on one
  line were blocked.
- Boolean flags set with = are read as gh reads them: --draft=true was
  blocked as not a draft, --approve=true and --request-changes=true
  went through.
- A line the tokenizer can't parse is refused when it mentions a gh pr
  or gh api command.
- review-gate, hook-commands and githooks tests run a second time with
  /bin/bash first on PATH when it is a different bash, so a construct
  bash 3.2 rejects fails a test. 21 new review-gate cases cover the
  above; against the previous gate, 11 fail under bash 5 and 67 under
  /bin/bash 3.2.

The changelog entries follow, and the #787 entry now quotes the Claude
Code docs for what CLAUDE_PROJECT_DIR is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
Docs review follow-up for the hooks and make ci-remote.

- AGENTS.md and the Claude Code guide say each gh api call is judged by
  its own endpoint and fields, that code a shell runs (including eval)
  is judged the same way, and that an unparseable line naming a gh pr
  or gh api command is refused.
- AGENTS.md's fail-closed paragraph links the exit-127 (non-blocking)
  and SubagentStop exit-2 claims to the Claude Code hooks docs, and
  points to the guide's "When a hook script is missing" section.
- The guide quotes the docs' definition of CLAUDE_PROJECT_DIR ("the
  project root where the session started") instead of paraphrasing it.
- The development guide's make ci-remote section: the local
  "==> make ci on ..." line comes first; the host's `uname -sm` is the
  first line the host prints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
Last review round on the agent gate's gh checks (#790).

- The queue of nested code stopped at 64 pieces and returned 0, so a gh
  command in the 65th went unjudged (63 `$(true)`, then
  `$(gh pr ready 12)`). Past the cap, a piece that mentions a gh pr or
  gh api command is now refused.
- gh resolves -R/--repo before the command (`gh -R o/r pr ready 12`),
  and the gate read only the word right after gh. It now steps over
  -R, --repo, -R<repo> and --repo=<repo> first, for pr and api both.
  The unparseable-line and past-the-cap checks know the same forms.
- A GraphQL query computed by a heredoc inside $(...) was unseen: the
  tokenizer keeps no heredoc body inside a substitution. When a graphql
  call's query= value is computed, the guarded mutation names are now
  matched against the code being judged, heredoc bodies included.
- A shell handed code only when -c was the next word. It now steps over
  options, including the value of -o, +o, -O, +O, --rcfile and
  --init-file, treats a short-option cluster containing c as -c (and not
  a long option such as --norc), and queues the first argument after
  them: `bash -e -c`, `bash -euo pipefail -c`, `bash --noprofile --norc
  -c`. The push check already read every word after a shell; the tests
  now pin that both agree on these forms.

21 new review-gate cases cover the above, plus look-alikes that must go
through (gh -R ... pr view, a heredoc GraphQL read, `bash --norc` running
a script, a read under `bash -o pipefail -c`). Against the previous
gate, 12 fail, under bash 5 and /bin/bash 3.2 alike. The changelog's
timing note no longer names a commit the squash will drop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
- The Claude Code guide's agent-bash-gate.sh row: an unparseable line is
  refused when it mentions a push or a gh pr / gh api command, not
  unconditionally, and a gh command beyond the 64 pieces of nested code
  the gate follows is refused unjudged.
- AGENTS.md and the guide's "API forms" bullet: `gh -R <repo> pr ...`
  counts as `gh pr ...`, options before a shell's -c don't hide its
  code, and a GraphQL query computed by a heredoc inside $(...) is read
  from that heredoc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
The gh checks queued each $(...) from the tokenizer's TK_SUBS text,
whose quotes are \002/\003 markers, and deleted the markers. Tokenized
again, the code split quoted arguments on spaces, `(` and `)` (#790):

- resp=$(gh api graphql -f query='mutation { markPullRequestReady...
  }') read as query=mutation and ended at the `(`, so every GraphQL
  mutation inside $(...) went through;
- url=$(gh pr create --draft --title "feat(x): ..." --body-file b.md)
  was blocked by the title lint, the title cut at the `(`;
- out=$(gh pr comment 12 --body "it's fixed") was refused as
  unparseable;
- n=$(grep -c 'gh pr ready' AGENTS.md) was read as a real gh pr ready.

The tokenizer now also keeps, in TK_CODE, the code inside each $(...)
and backtick substitution exactly as written, quotes and heredoc bodies
included, and the gh checks queue that. TK_SUBS keeps its marker form
for the push check. With the heredoc bodies kept, a heredoc a shell
reads inside $(...) is now judged too. The other code the gh checks
re-read (a shell's -c argument, eval's words, heredoc bodies) was
already the text the shell itself runs.

7 new review-gate cases, under bash 5 and /bin/bash 3.2: the four above
plus a scoped title, a bad title still linted inside $(...), and a gh pr
ready in a heredoc read by bash inside $(...). 6 of them fail against
the previous gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: ccba0aed-9024-49ee-a715-7ce71cceb67a

📥 Commits

Reviewing files that changed from the base of the PR and between 6c9a253 and ccbea68.


📒 Files selected for processing (5)
  • .claude/hooks/agent-bash-gate.sh
  • .claude/hooks/review-gate.test.sh
  • AGENTS.md
  • CHANGELOG.md
  • docs/src/content/docs/claude-code.md

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📜 Recent review details
🧰 Additional context used
📚 Code guidelines (3)
.github/copilot-instructions.md — auto-discovered
AGENTS.md — auto-discovered
docs/src/content/docs/development.md — auto-discovered

📓 Path-based instructions (3)
See [AGENTS.md](../AGENTS.md) for project conventions, architecture notes, and AI agent instructions.

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • AGENTS.md

Source excerpt: **Never hard-wrap prose.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • CHANGELOG.md
  • AGENTS.md
  • docs/src/content/docs/claude-code.md

Source excerpt: Run `make fix` to apply them.

📄 CodeRabbit inference engine (docs/src/content/docs/development.md)

Files:

  • CHANGELOG.md
  • AGENTS.md
  • docs/src/content/docs/claude-code.md


🪛 LanguageTool
AGENTS.md

[style] ~255-~255: Consider using the typographical ellipsis character here instead.
Context: ...e/gh pr new, any gh apiwrite to/repos/.../pulls//requested_reviewers`, or the ...

(ELLIPSIS)




🔇 Additional comments (5)
.claude/hooks/agent-bash-gate.sh (1)

92-92: LGTM!

Also applies to: 98-99, 101-102, 104-105, 114-114


.claude/hooks/review-gate.test.sh (1)

599-613: LGTM!


AGENTS.md (1)

255-255: LGTM!


CHANGELOG.md (1)

123-125: LGTM!


docs/src/content/docs/claude-code.md (1)

111-111: LGTM!





📝 Summary

Summary by CodeRabbit

  • New Features
    • Added make ci-remote to run CI on an SSH host or Docker container. Results are recorded locally only when the validated code matches the current checkout.
  • Bug Fixes
    • Strengthened safeguards for pull request creation, title validation, reviews, reviewer assignments, draft readiness, and merges across command-line and API workflows.
    • Improved Git hook behavior when scripts, marker files, or a Makefile are unavailable, including clearer guidance when remote CI is needed.
  • Documentation
    • Updated CI, Git hook, and pull request workflow guidance, including instructions for remote CI runs.

Walkthrough

The pull request expands Claude Code command gates, adds remote CI execution that can write local validation markers, and changes Git hook behavior for worktrees without a Makefile. It also adds tests and documentation for these changes.

Changes

Claude Code Command Gates

Layer / File(s) Summary
Inspect GitHub commands
.claude/hooks/agent-bash-gate.sh
The Bash gate inspects additional gh pr and gh api REST and GraphQL operations. It scans nested executable code, requires draft creation, validates available literal titles, and blocks specified review, reviewer, readiness, and merge actions.
Test command-gate behavior
.claude/hooks/review-gate.test.sh
Tests cover restricted and allowed commands, shell execution forms, nested code, REST and GraphQL fields, and title checks.
Handle unavailable Claude Code hooks
.claude/settings.json, .claude/hooks/hook-commands.test.sh, AGENTS.md, docs/src/content/docs/claude-code.md, CHANGELOG.md
Settings check whether hook scripts exist and are executable. Fallback commands block or report missing hooks with event-specific exit codes. Tests and documentation cover these cases and gate behavior.

Remote CI

Layer / File(s) Summary
Run CI remotely and validate results
scripts/ci/remote-ci.sh, scripts/ci-marker.sh
make ci-remote sends HEAD to an SSH host or Docker container and runs make tools and make ci. The script writes local markers and provenance only after its checks confirm the reported tree, CI result, and local worktree state.
Test remote runs and marker conditions
scripts/ci/remote-ci.test.sh, .githooks/pre-push
Stand-in tests cover successful and failed runs, tree and worktree changes, interruption, cleanup, and marker provenance.
Expose and document remote CI
Makefile, AGENTS.md, CONTRIBUTING.md, CHANGELOG.md, docs/src/content/docs/development.md
The Makefile adds the target and test registration. The validation guidance describes remote runs as a source of CI markers.

Git Hook Behavior

Layer / File(s) Summary
Update pre-commit and pre-push checks
.githooks/pre-commit, .githooks/pre-push
Pre-commit skips verification when the worktree has no Makefile and runs verification when marker checks cannot be completed. Pre-push skips commits without a Makefile and reports provenance when available.
Test and register Git hook validation
scripts/githooks.test.sh, Makefile
Scratch-worktree tests cover hook outcomes. The Makefile registers hook tests and includes tracked Git hooks in shell linting.
Document worktree hook behavior
docs/src/content/docs/claude-code.md, CONTRIBUTING.md, CHANGELOG.md
The documentation describes hook behavior across worktrees and the updated marker checks.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Makefile
  participant RemoteCIScript
  participant SSHorDocker
  participant RemoteCheckout
  participant LocalMarkers
  Makefile->>RemoteCIScript: invoke ci-remote
  RemoteCIScript->>SSHorDocker: send HEAD bundle and remote script
  SSHorDocker->>RemoteCheckout: create temporary checkout
  RemoteCheckout->>RemoteCheckout: run make tools and make ci
  RemoteCheckout-->>RemoteCIScript: report tested tree and result
  RemoteCIScript->>LocalMarkers: write markers and provenance after validation
Loading

Suggested reviewers: taitelee


Merge Risk: ⚪ Minimal · up to ccbea

No actionable merge blocker was established. Normal validation remains appropriate before merging.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 48 functions across 7 files. (3 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check Passed The description clearly explains both major changes, affected hooks, design decisions, testing, and known limitations.
Linked Issues check Passed The description identifies and addresses the linked issues #764, #678, #787, and #790, with implementation and test coverage details.
Out of Scope Changes check Passed The listed code, tests, documentation, and hook changes support the stated CI and fail-closed hook objectives. No unrelated changes are evident.
Title check Passed The title concisely and accurately identifies the two primary changes: adding make ci-remote and making repository hooks fail closed.

Full details: Docstring Coverage

Explanation

Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 48 functions across 7 files. (3 skipped: 3 unsupported.)



  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

✨ Simplify code
  • Commit to this branch
  • Create a new PR


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added documentation Improvements or additions to documentation area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

📚 Docs preview is live → https://f8e452ba-wavehouse-docs.wave-rf.workers.dev

  • Commit — ccbea68: fix(hooks): block reviewer and assignee flags on PR create
  • Author — @EricAndrechek, Claude Opus 5.5
  • Committed — 2026-10-09 15:53 (UTC-04:00)
  • Deployed — 2026-10-09 16:41 EDT

@github-code-quality

github-code-quality Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: Go

Go

The overall line coverage in commit ccbea68 in the ci/remote-make-ci-v2 branch remains at 93%, unchanged from commit 3582039 in the main branch.

Show a line coverage summary of the most impacted files.
File main 3582039 ci/remote-make-ci-v2 ccbea68 +/-
internal/mq/embedded.go 90% 90% 0%
internal/ingest/worker.go 97% 97% 0%
internal/mq/external.go 85% 85% 0%
internal/mq/ext...nal_consumer.go 88% 89% +1%

Updated October 09, 2026 20:45 UTC

Brings in #789, #791, #786 and the PRs merged since. CHANGELOG.md
conflicted only because new Fixed entries landed at the same spot; all
are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
@EricAndrechek

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Both PRs added their Fixed entries at the top of the section after
merging main, so whichever merged second would conflict. These now sit
below the TestClaims entry from main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
@EricAndrechek

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: f7308005-7ac3-41ba-b7a9-80ee477b8694
📥 Commits

Reviewing files that changed from the base of the PR and between b9069da and 6c9a253.

📒 Files selected for processing (16)
  • .claude/hooks/agent-bash-gate.sh
  • .claude/hooks/hook-commands.test.sh
  • .claude/hooks/review-gate.test.sh
  • .claude/settings.json
  • .githooks/pre-commit
  • .githooks/pre-push
  • AGENTS.md
  • CHANGELOG.md
  • CONTRIBUTING.md
  • Makefile
  • docs/src/content/docs/claude-code.md
  • docs/src/content/docs/development.md
  • scripts/ci-marker.sh
  • scripts/ci/remote-ci.sh
  • scripts/ci/remote-ci.test.sh
  • scripts/githooks.test.sh

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📜 Review details
🧰 Additional context used
📚 Code guidelines (3)
.github/copilot-instructions.md — auto-discovered
docs/src/content/docs/development.md — auto-discovered
docs/src/content/docs/claude-code.md — auto-discovered
📓 Path-based instructions (3)
See [AGENTS.md](../AGENTS.md) for project conventions, architecture notes, and AI agent instructions.

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • AGENTS.md
Source excerpt: Run `make fix` to apply them.

📄 CodeRabbit inference engine (docs/src/content/docs/development.md)

Files:

  • CONTRIBUTING.md
  • CHANGELOG.md
  • docs/src/content/docs/development.md
  • docs/src/content/docs/claude-code.md
  • AGENTS.md
Source excerpt: The `.claude/` layout is intentionally small.

📄 CodeRabbit inference engine (docs/src/content/docs/claude-code.md)

Files:

  • .githooks/pre-commit
  • .githooks/pre-push
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Wave-RF/WaveHouse

Timestamp: 2026-10-09T19:33:38.774Z
Learning: Source excerpt:
# AGENTS.md — AI Agent Instructions for WaveHouse

## Documentation Sync

Every code change should update the corresponding docs in the same PR. A code change without its doc update is incomplete.
🪛 ast-grep (0.45.3)
scripts/ci/remote-ci.test.sh

[warning] 131-131: Writing to or reading from a hardcoded, predictable path under /tmp is vulnerable to symlink and TOCTOU attacks: a local attacker can pre-create the file (or a symlink pointing elsewhere) and hijack or corrupt the contents. Generate a unique, unpredictable temporary file with mktemp instead, e.g. tmpfile="$(mktemp)" (or mktemp -d for directories) and reference "$tmpfile".
Context: "$repo/tmp/ci-passed-tree-$1"
Note: [CWE-377] Insecure Temporary File.

(predictable-tmp-file-bash)

🪛 LanguageTool
AGENTS.md

[style] ~176-~176: Since ownership is already implied, this phrasing may be redundant.
Context: ...in each worktree, so each worktree runs its own branch's hooks, and a worktree whose br...

(PRP_OWN)


[style] ~249-~249: Since ownership is already implied, this phrasing may be redundant.
Context: ...reads). Each gh api call is judged by its own endpoint (its first argument that isn't...

(PRP_OWN)


[style] ~249-~249: Since ownership is already implied, this phrasing may be redundant.
Context: ...s first argument that isn't a flag) and its own fields, so a comment body that names an...

(PRP_OWN)


[style] ~251-~251: This sentence is over 40 words long. Consider splitting it up, as shorter sentences make the text easier to read.
Context: ...alid; it prints the reason on failure). .claude/hooks/agent-bash-gate.sh runs the same check on gh pr create / gh pr new / gh pr edit --title, and on the title= field of a gh api call that opens or edits a PR, so a malformed title is caught locally before the PR exists. The rule has a single source of truth —...

(TOO_LONG_SENTENCE)


[style] ~255-~255: Consider using the typographical ellipsis character here instead.
Context: ...ssignee , any gh apiwrite to/repos/.../pulls//requested_reviewers`, or the ...

(ELLIPSIS)

🔇 Additional comments (17)
.claude/settings.json (2)

39-39: Block every command when the payload cannot be parsed, as the message says.

The jq fallback runs only when jq fails. In that case c becomes git push, and the guard blocks the command, so this path works. The message says that without jq every command is blocked, and the test at Line 207 of hook-commands.test.sh confirms that behavior. One more case is also covered: a payload that is not JSON makes jq exit non-zero, so the guard blocks it. I found no defect on this line.


51-55: LGTM!

Also applies to: 65-65

.claude/hooks/hook-commands.test.sh (1)

1-321: LGTM!

AGENTS.md (1)

9-9: LGTM!

Also applies to: 172-179, 243-244, 249-255, 295-295

CHANGELOG.md (1)

56-56: LGTM!

Also applies to: 123-125

docs/src/content/docs/claude-code.md (1)

27-27: LGTM!

Also applies to: 39-40, 46-47, 52-58, 109-111, 119-130

docs/src/content/docs/development.md (1)

22-22: LGTM!

Also applies to: 343-358, 552-552, 568-568

scripts/ci/remote-ci.sh (1)

1-160: LGTM!

scripts/ci/remote-ci.test.sh (1)

1-341: LGTM!

Makefile (1)

99-99: LGTM!

Also applies to: 432-436, 516-540, 631-637, 651-651, 1048-1057

.githooks/pre-commit (1)

4-36: LGTM!

.githooks/pre-push (1)

7-24: LGTM!

Also applies to: 41-53, 59-66, 78-78

scripts/githooks.test.sh (1)

1-180: LGTM!

scripts/ci-marker.sh (1)

6-8: LGTM!

CONTRIBUTING.md (1)

40-40: LGTM!

.claude/hooks/agent-bash-gate.sh (1)

44-60: LGTM!

Also applies to: 62-72, 74-100, 111-120, 122-318, 363-365, 410-410, 426-445, 458-466, 576-576, 933-936, 1000-1001

.claude/hooks/review-gate.test.sh (1)

12-29: LGTM!

Also applies to: 516-664

Comment thread .claude/hooks/agent-bash-gate.sh Resolved
The agent gate blocked adding reviewers or assignees on edit, but creating a PR with --reviewer/-r or --assignee/-a went through. Block them on create and its alias new with the reviewer message, glued short forms included; on review, -r and -a still mean --request-changes and --approve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wjv2nu841pxTMdUWSAPmDH
@EricAndrechek

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@EricAndrechek

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release documentation Improvements or additions to documentation

Projects

Archived in project

1 participant